########################################################################
#  Searx-Qt - Lightweight desktop application for Searx.
#  Copyright (C) 2020-2022  CYBERDEViL
#
#  This file is part of Searx-Qt.
#
#  Searx-Qt is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Searx-Qt is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
########################################################################

import sys
from copy import deepcopy

from PyQt5.QtWidgets import (
    QApplication,
    QAction,
    QSplitter,
    QMenuBar,
    QMenu,
    QMainWindow,
    QHBoxLayout,
    QVBoxLayout,
    QWidget,
    QStatusBar,
    QLabel,
    QMessageBox
)

from PyQt5.QtCore import (
    Qt,
    QSettings,
    QByteArray,
    QSize
)

from searxqt.models.instances import (
    Stats2InstancesModel,
    UserInstancesModel,
    Stats2EnginesModel,
    UserEnginesModel,
    Stats2Model,
    InstanceModelFilter,
    InstanceSelecterModel,
    PersistentInstancesModel,
    PersistentEnginesModel,
    InstancesModelTypes
)

from searxqt.models.settings import SettingsModel
from searxqt.models.search import (
    SearchModel,
    UserInstancesHandler,
    SearchStatus
)
from searxqt.models.profiles import Profiles, ProfileItem

from searxqt.views.instances import InstancesView
from searxqt.views.settings import SettingsWindow
from searxqt.views.search import SearchContainer
from searxqt.views import about
from searxqt.views.profiles import ProfileChooserDialog

from searxqt.core.customAnchorCmd import AnchorCMD
from searxqt.core.images import ImagesSettings
from searxqt.core.requests import RequestsHandler, RequestSettingsWithParent
from searxqt.core.guard import Guard

from searxqt.translations import _
from searxqt.version import __version__

from searxqt import PROFILES_PATH, SETTINGS_PATH

from searxqt.core import log


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        QMainWindow.__init__(self, *args, **kwargs)
        self.setWindowTitle("Searx-Qt")

        self._handler = None
        self._settingsWindow = None

        # Request handler
        self._requestHandler = RequestsHandler()
        imgRequestSettings = RequestSettingsWithParent(self._requestHandler.settings)
        self._imgRequestHandler = RequestsHandler(imgRequestSettings)

        self._settingsModel = SettingsModel(self._requestHandler.settings,
                                            self._imgRequestHandler.settings,
                                            self)

        # Persistent models
        self._persistantInstancesModel = PersistentInstancesModel()
        self._persistantEnginesModel = PersistentEnginesModel()

        # Profiles
        self._profiles = Profiles()

        self.instanceFilter = InstanceModelFilter(
            self._persistantInstancesModel, self
        )
        self.instanceSelecter = InstanceSelecterModel(self.instanceFilter)
        self._searchModel = SearchModel(self._requestHandler, self)

        # Guard
        self._guard = Guard()

        # -- Menu bar
        menubar = QMenuBar(self)

        # Menu file
        menuFile = QMenu(menubar)
        menuFile.setTitle(_("File"))

        saveAction = QAction(_("Save"), menuFile)
        menuFile.addAction(saveAction)
        saveAction.setShortcut('Ctrl+S')
        saveAction.triggered.connect(self.saveSettings)

        actionExit = QAction(_("Exit"), menuFile)
        menuFile.addAction(actionExit)
        actionExit.setShortcut('Ctrl+Q')
        actionExit.triggered.connect(self.close)

        menubar.addAction(menuFile.menuAction())

        # Menu settings
        settingsAction = QAction(_("Settings"), menubar)
        menubar.addAction(settingsAction)
        settingsAction.triggered.connect(self._openSettingsWindow)

        # Menu profiles
        profilesAction = QAction(_("Profiles"), menubar)
        menubar.addAction(profilesAction)
        profilesAction.triggered.connect(self._openProfileChooser)

        # Menu about dialog
        aboutAction = QAction(_("About"), menubar)
        menubar.addAction(aboutAction)
        aboutAction.triggered.connect(self._openAboutDialog)

        self.setMenuBar(menubar)
        # -- End menu bar

        # -- Status bar
        self.statusBar = QStatusBar(self)

        statusWidget = QWidget(self)
        statusLayout = QHBoxLayout(statusWidget)

        self._handlerThreadStatusLabel = QLabel(self)
        statusLayout.addWidget(self._handlerThreadStatusLabel)

        self._statusInstanceLabel = QLabel(self)
        statusLayout.addWidget(self._statusInstanceLabel)

        self.statusBar.addPermanentWidget(statusWidget)

        self.setStatusBar(self.statusBar)
        # -- End status bar

        centralWidget = QWidget(self)
        layout = QVBoxLayout(centralWidget)
        self.setCentralWidget(centralWidget)

        self.splitter = QSplitter(centralWidget)
        self.splitter.setOrientation(Qt.Horizontal)
        layout.addWidget(self.splitter)

        self.searchContainer = SearchContainer(
            self._searchModel,
            self._imgRequestHandler,
            self.instanceFilter,
            self.instanceSelecter,
            self._persistantEnginesModel,
            self._guard,
            self.splitter
        )
        self.instancesWidget = InstancesView(
            self.instanceFilter,
            self.instanceSelecter,
            self.splitter
        )

        self.instanceSelecter.instanceChanged.connect(self.__instanceChanged)
        self._searchModel.statusChanged.connect(self.__searchStatusChanged)

        self.__profileChooserInit()
        self.resize(800, 600)
        self.loadSharedSettings()
        self.loadProfile()

    def __instanceChanged(self, url):
        self._statusInstanceLabel.setText(f"<b>{_('Instance')}:</b> {url}")

    # Disable/enable instances view on search status change.
    def __searchStatusChanged(self, status):
        if status == SearchStatus.Busy:
            self.instancesWidget.setEnabled(False)
        else:
            self.instancesWidget.setEnabled(True)

    def closeEvent(self, event=None):
        # Disable everything.
        self.setEnabled(False)

        # Wait till all threads finished
        if self.searchContainer.isBusy():
            log.info("- Waiting for search thread to finish...", self)
            self.searchContainer.cancelAll()
            log.info("- Search thread finished.")

        if self._handler and self._handler.hasActiveJobs():
            log.info(
                "- Waiting for update instances thread to finish...",
                self
            )
            self._handler.cancelAll()
            log.info("- Instances update thread finished.", self)

        self.saveSettings()
        log.info("- Settings saved.", self)

        # Remove currently active profile id from the active list.
        self._profiles.setProfile(
            self._profiles.settings(),
            ProfileItem()
        )

        QApplication.closeAllWindows()
        log.info("Bye!", self)

    def _openAboutDialog(self):
        about.show(self)

    def __execProfileChooser(self):
        """ This only sets the profile, it does not load it.

        Returns True on success, False when something went wrong.
        """
        profiles = self._profiles
        profilesSettings = profiles.settings()
        profiles.loadProfiles(profilesSettings)  # read profiles.conf

        dialog = ProfileChooserDialog(profiles)
        if dialog.exec():
            currentProfile = profiles.current()
            selectedProfile = dialog.selectedProfile()

            # Save current profile if one is set.
            if currentProfile.id:
                self.saveProfile()

            profiles.setProfile(
                profilesSettings,
                selectedProfile
            )
        else:
            self.__finalizeProfileChooser(dialog)
            return False

        self.__finalizeProfileChooser(dialog)
        return True

    def __profileChooserInit(self):
        profiles = self._profiles
        profilesSettings = profiles.settings()
        profiles.loadProfiles(profilesSettings)  # read profiles.conf
        activeProfiles = profiles.getActiveProfiles(profilesSettings)
        defaultProfile = profiles.default()

        if defaultProfile is None or defaultProfile.id in activeProfiles:
            if not self.__execProfileChooser():
                sys.exit()
        else:
            # Load default profile.
            profiles.setProfile(
                profilesSettings,
                defaultProfile
            )

    def _openProfileChooser(self):
        if self._handler and self._handler.hasActiveJobs():
            QMessageBox.information(
                self,
                _("Instances update thread active"),
                _("Please wait until instances finished updating before\n"
                  "switching profiles.")
            )
            return

        if self.__execProfileChooser():
            self.loadProfile()

    def __finalizeProfileChooser(self, dialog):
        """ Profiles may have been added or removed.
        - Store profiles.conf
        - Remove removed profile conf files
        """
        self._profiles.saveProfiles()
        self._profiles.removeProfileFiles(dialog.removedProfiles())

    def _openSettingsWindow(self):
        if not self._settingsWindow:
            self._settingsWindow = SettingsWindow(
                self._settingsModel,
                self._searchModel,
                self._guard
            )
            self._settingsWindow.resize(self.__lastSettingsWindowSize)
        self._settingsWindow.show()
        self._settingsWindow.activateWindow()  # Bring it to front
        self._settingsWindow.closed.connect(self._delSettingsWindow)

    def _delSettingsWindow(self):
        self._settingsWindow.closed.disconnect()
        self._settingsWindow.deleteLater()
        self._settingsWindow = None

    def __handlerThreadChanged(self):
        self._handlerThreadStatusLabel.setText(
            self._handler.currentJobStr()
        )

    def loadProfile(self):
        profile = self._profiles.current()
        self.setWindowTitle(f"Searx-Qt - {profile.name}")
        profileSettings = QSettings(PROFILES_PATH, profile.id, self)

        # Clean previous stuff
        if self._handler:
            self._handler.threadStarted.disconnect(
                self.__handlerThreadChanged
            )
            self._handler.threadFinished.disconnect(
                self.__handlerThreadChanged
            )
            self._handler.deleteLater()
            self._handler = None

        if self._settingsWindow:
            self._delSettingsWindow()

        self.searchContainer.reset()

        # Set new models
        if profile.type == InstancesModelTypes.Stats2:
            self._handler = Stats2Model(self._requestHandler, self)
            instancesModel = Stats2InstancesModel(self._handler, parent=self)
            enginesModel = Stats2EnginesModel(self._handler, parent=self)

            self._settingsModel.loadSettings(
                profileSettings.value('settings', dict(), dict),
                stats2=True
            )

        elif profile.type == InstancesModelTypes.User:
            self._handler = UserInstancesHandler(self._requestHandler, self)
            instancesModel = UserInstancesModel(self._handler, parent=self)
            enginesModel = UserEnginesModel(self._handler, parent=self)

            self._settingsModel.loadSettings(
                profileSettings.value('settings', dict(), dict),
                stats2=False
            )
        else:
            print(f"ERROR unknown profile type '{profile.type}'")
            sys.exit(1)

        self._persistantInstancesModel.setModel(instancesModel)
        self._persistantEnginesModel.setModel(enginesModel)

        self._handler.setData(
            profileSettings.value('data', dict(), dict)
        )

        self._handler.threadStarted.connect(self.__handlerThreadChanged)
        self._handler.threadFinished.connect(self.__handlerThreadChanged)

        # Load settings
        self.instanceFilter.loadSettings(
            profileSettings.value('instanceFilter', dict(), dict)
        )

        self.instancesWidget.loadSettings(
            profileSettings.value('instancesView', dict(), dict)
        )

        self.instanceSelecter.loadSettings(
            profileSettings.value('instanceSelecter', dict(), dict)
        )

        self.searchContainer.loadSettings(
            profileSettings.value('searchContainer', dict(), dict)
        )

        self._searchModel.loadSettings(
            profileSettings.value('searchModel', dict(), dict)
        )

        ImagesSettings.deserialize(
            profileSettings.value('imgSettings', dict(), dict)
        )

        # Guard
        self._guard.deserialize(profileSettings.value('Guard', dict(), dict))

        # Load main window splitter state (between search and instances)
        self.splitter.restoreState(
            profileSettings.value('splitterState', QByteArray(), QByteArray)
        )

        # Custom Anchor commands
        AnchorCMD.deserialize(
            profileSettings.value('customAnchorCmd', dict(), dict)
        )

        # Load defaults for new profiles.
        if profile.preset:
            from searxqt import defaults
            preset = defaults.Presets[profile.preset]
            # Settings
            if profile.type == InstancesModelTypes.Stats2:
                self._settingsModel.loadSettings(
                    deepcopy(preset.get('settings', {})),
                    stats2=True
                )
            elif profile.type == InstancesModelTypes.User:
                self._settingsModel.loadSettings(
                    deepcopy(preset.get('settings', {})),
                    stats2=False
                )
            # Guard
            self._guard.deserialize(
                deepcopy(preset.get('guard', {}))
            )
            # InstancesView
            self.instancesWidget.loadSettings(
                deepcopy(preset.get('instancesView', {}))
            )
            # InstancesFilter
            self.instanceFilter.loadSettings(
                deepcopy(preset.get('instancesFilter', {}))
            )
            # Instances
            self._handler.setData(
                deepcopy(preset.get('data', {}))
            )
            del preset
            del defaults

    def saveProfile(self):
        """ Save current profile
        """
        profile = self._profiles.current()
        profileSettings = QSettings(PROFILES_PATH, profile.id, self)
        profileSettings.setValue(
            'settings', self._settingsModel.saveSettings()
        )
        profileSettings.setValue(
            'instanceFilter', self.instanceFilter.saveSettings()
        )
        profileSettings.setValue(
            'instancesView', self.instancesWidget.saveSettings()
        )
        profileSettings.setValue(
            'instanceSelecter', self.instanceSelecter.saveSettings()
        )
        profileSettings.setValue(
            'searchContainer', self.searchContainer.saveSettings()
        )
        profileSettings.setValue(
            'searchModel', self._searchModel.saveSettings()
        )
        profileSettings.setValue(
            'imgSettings', ImagesSettings.serialize()
        )

        # Guard
        profileSettings.setValue('Guard', self._guard.serialize())

        # Store the main window splitter state (between search and instances)
        profileSettings.setValue('splitterState', self.splitter.saveState())

        # Custom Anchor commands
        profileSettings.setValue('customAnchorCmd', AnchorCMD.serialize())

        # Store searx-qt version (for backward compatibility)
        profileSettings.setValue('version', __version__)

        if self._handler:
            profileSettings.setValue('data', self._handler.data())

    def loadSharedSettings(self):
        """ Load shared settings
        """
        settings = QSettings(SETTINGS_PATH, 'shared', self)
        self.resize(
            settings.value('windowSize', QSize(), QSize)
        )

        self.__lastSettingsWindowSize = settings.value(
            'settingsWindowSize',
            QSize(400, 400),
            QSize
        )

    def saveSettings(self):
        # save current profile
        if self._profiles.current().id:
            self.saveProfile()

        # shared.conf
        settings = QSettings(SETTINGS_PATH, 'shared', self)
        settings.setValue('windowSize', self.size())

        if self._settingsWindow:
            settings.setValue(
                'settingsWindowSize',
                self._settingsWindow.size()
            )
